/*
 *  libKara - Library for basic instant messaging functions
 *  Copyright (C) 2007 Markus Karnik
 *
 *  This program is free software; you can redistribute it and/or modify it 
 *  under the terms of the GNU General Public License as published by the
 *  Free Software Foundation; either version 3 of the License, 
 *  or (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful, but
 *  WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 *  or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 
 *  for more details.
 *
 *  You should have received a copy of the GNU General Public License along 
 *  with this program; if not, see <http://www.gnu.org/licenses/>.
 *  
 */

package de.karnik.libkara.proto.xmpp;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;

import de.karnik.helper.StringFunctions;
import de.karnik.helper.LogFunctions;
import de.karnik.libkara.pluginspecs.BaseProtocol;
import de.karnik.libkara.pluginspecs.ProtocolSettings;
import de.karnik.libkara.proto.ProtocolException;
import de.karnik.libkara.proto.xmpp.net.XmppConnectionManager;
import de.karnik.libkara.proto.xmpp.packages.XmlPackageStorage;
import de.karnik.libkara.proto.xmpp.packages.XmppIQChildRoster;
import de.karnik.libkara.proto.xmpp.packages.XmppIQChildSession;
import de.karnik.libkara.proto.xmpp.packages.XmppIQStanza;
import de.karnik.libkara.proto.xmpp.packages.XmppMessageStanza;
import de.karnik.libkara.proto.xmpp.packages.XmppPresenceStanza;
import de.karnik.libkara.proto.xmpp.packages.XmppStanzaReaderListener;
import de.karnik.libkara.roster.ImContact;
import de.karnik.libkara.roster.ImMessage;


/**
 * XmppProtocol represents the basic funtionality of the xmpp protocol.
 * 
 * @author Markus Karnik
 * @version 1.0
 * @since 1.0
 */
public class XmppProtocol extends BaseProtocol implements XmppParserListener, XmppThreadListener, XmppStanzaReaderListener {

	/**
	 * The protocol major version.
	 */
	private static final int MAJOR_VERSION = 0;
	/**
	 * The protocol minor version.
	 */
	private static final int MINOR_VERSION = 1;
	
	/**
	 * The XMPP connection mananger.
	 */
	private XmppConnectionManager connectionManager = null;
	
	/**
	 * The XMPP parser thread.
	 */
	private XmppParser xmppParser = null;
	/**
	 * The XMPP writer object.
	 */
	private BufferedWriter xmppWriter = null;
	/**
	 * The XMPP reader object.
	 */
	private BufferedReader xmppReader = null;
	/**
	 * The XMPP package writer thread.
	 */
	private XmppWriterThread writerThread = null;
	/**
	 * The XMPP stanza reader thread object.
	 */
	private XmppStanzaReaderThread stanzaReaderThread = null;
	/**
	 * The XMPP XML package storage object.
	 */
	private XmlPackageStorage storage = null;
		
	/**
	 * Converts KaraIM status value to XMPP status value.
	 * 
	 * @param karaStatus The XMPP status value.
	 * @return The XMPP status value as string.
	 */
	public static String getXmppStatus( int karaStatus ) {
				
		switch( karaStatus ) {
			case BaseProtocol.STATUS_AVAILABLE:
				return XmppPresenceStanza.STATUS_CHAT;
			case BaseProtocol.STATUS_DND:
				return XmppPresenceStanza.STATUS_DND;
			case BaseProtocol.STATUS_AWAY:
				return XmppPresenceStanza.STATUS_AWAY;
			default:
				return XmppPresenceStanza.STATUS_CHAT;
		}
	}
	
	/**
	 * Converts XMPP status value to KaraIM status value.
	 * 
	 * @param xmppStatus The XMPP status value.
	 * @return The Kara status value as integer.
	 */
	public static int getKaraStatus( String xmppStatus ) {
			
		LogFunctions.log( "getKaraStatus " + xmppStatus );
		
		if( xmppStatus.equals( XmppPresenceStanza.STATUS_AWAY ) ) {
			return BaseProtocol.STATUS_AWAY;
		} else if( xmppStatus.equals( XmppPresenceStanza.STATUS_CHAT ) ) {
			return BaseProtocol.STATUS_AVAILABLE;
		} else if( xmppStatus.equals( XmppPresenceStanza.STATUS_DND ) ) {
			return BaseProtocol.STATUS_DND;
		} else if( xmppStatus.equals( XmppPresenceStanza.STATUS_XA ) ) {
			return BaseProtocol.STATUS_AWAY;
		}
		
		return BaseProtocol.STATUS_OFFLINE;
	}
	
	/**
	 * Strips the resource ( e.g. "/home" ) from the userid.
	 * 
	 * @param userId The userId to use.
	 * @return The stripped userId.
	 */
	public static String stripResourceFromUserId( String userId ) {
		int pos = 0;
		if( (  pos = userId.indexOf( ( int )'/' ) ) > 0 )
			userId = userId.substring( 0, pos );
		
		return userId;
	}
	
	/* (non-Javadoc)
	 * @see de.karnik.libkara.pluginspecs.BaseProtocol#proto_configure(de.karnik.libkara.pluginspecs.ProtocolSettings)
	 */
	public void proto_configure( ProtocolSettings protocolSettings ) throws ProtocolException {
		XmppSettings xmppSettings;
		
		if( protocolSettings == null ) {
			xmppSettings = new XmppSettings();
		} else {
			if( protocolSettings instanceof XmppSettings )
				xmppSettings = ( XmppSettings )protocolSettings;
			else
				xmppSettings = new XmppSettings( protocolSettings );
		}
		
		this.connectionManager =  new XmppConnectionManager( xmppSettings );
		this.storage =  new XmlPackageStorage();
	}
	
	/* (non-Javadoc)
	 * @see de.karnik.libkara.pluginspecs.BaseProtocol#proto_connect()
	 */
	public void proto_connect() throws ProtocolException {

		try {
			this.connectionManager.connect();
		} catch ( Exception e ) {
			throw new ProtocolException( e );
		}
	}
		
	/* (non-Javadoc)
	 * @see de.karnik.libkara.pluginspecs.BaseProtocol#proto_login()
	 */
	public void proto_login() throws ProtocolException {
		this.xmppWriter = this.connectionManager.getXmppWriter();
		this.xmppReader = this.connectionManager.getXmppReader();
		
		this.writerThread = new XmppWriterThread( StringFunctions.getUniqueString(), this.xmppWriter, this.storage );
		this.writerThread.setThreadListener( this );
		this.writerThread.start();
		
		try {
			
			this.stanzaReaderThread = new XmppStanzaReaderThread( StringFunctions.getUniqueString(), 
					this.storage, 
					this.connectionManager.getXmppSettings() );
			
			this.stanzaReaderThread.addXmppStanzaListener( this );
			this.stanzaReaderThread.setThreadListener( this );
			this.stanzaReaderThread.start();
			
			this.xmppParser =  new XmppParser( StringFunctions.getUniqueString(), this.xmppReader, 
												this.connectionManager.getXmppSettings(), this.storage );
			this.xmppParser.setXmppParserListener( this );
			this.xmppParser.setXmppThreadListener( this );
			this.xmppParser.start();
			
			this.xmppParser.sendStandardHeader();
			
		} catch ( Exception e) {
			throw new ProtocolException( e );
		}
	}
	
	/* (non-Javadoc)
	 * @see de.karnik.libkara.pluginspecs.BaseProtocol#proto_isConnected()
	 */
	public boolean proto_isConnected() throws ProtocolException {
		if( this.connectionManager != null )
			return this.connectionManager.isConnected();
		
		return false;
	}

	/* (non-Javadoc)
	 * @see de.karnik.libkara.pluginspecs.BaseProtocol#proto_logoff()
	 */
	public void proto_logoff() throws ProtocolException {

		XmlPackageStorage.enableHeartbeat = false;
		
		if( this.xmppParser != null && this.xmppParser.isAlive() )
			this.xmppParser.shutdown();
		
		// wait 500 ms for sending the </stream:stream> - Tag
		try {
			Thread.sleep( 500 );
		} catch ( InterruptedException e ) {
			throw new ProtocolException( e );
		}
		
		if( this.stanzaReaderThread != null && this.stanzaReaderThread.isAlive() )
			this.stanzaReaderThread.shutdown();
		if( this.writerThread != null && this.writerThread.isAlive() )
			this.writerThread.shutdown();
		
		if( this.storage != null )
			this.storage.clearStorage();
		
		try {
			this.writerThread.join( 200 );
			this.stanzaReaderThread.join( 200 );
			this.xmppParser.join( 200 );
		} catch (InterruptedException e) {
			throw new ProtocolException( e );
		}
		
		this.writerThread = null;
		this.stanzaReaderThread = null;
		this.xmppParser = null;
		
		this.connectionManager.getXmppSettings().putBooleanData( XmppSettings.BOOLEAN_VALUE_LOGGED_IN, false );
		
		this.connectionManager.getXmppSettings().putBooleanData( XmppSettings.BOOLEAN_VALUE_TLS_ESTABLISHED, false );
		this.connectionManager.getXmppSettings().putBooleanData( XmppSettings.BOOLEAN_VALUE_TLS_ENABLED, false );
		this.connectionManager.getXmppSettings().putBooleanData( XmppSettings.BOOLEAN_VALUE_TLS_REQUIRED, false );
		
		this.connectionManager.getXmppSettings().putBooleanData( XmppSettings.BOOLEAN_VALUE_BIND_REQUIRED, false );
		this.connectionManager.getXmppSettings().putBooleanData( XmppSettings.BOOLEAN_VALUE_BOUND, false );
			
		this.connectionManager.getXmppSettings().putBooleanData( XmppSettings.BOOLEAN_VALUE_SESSION_REQUIRED, false );
		this.connectionManager.getXmppSettings().putBooleanData( XmppSettings.BOOLEAN_VALUE_SESSION_ESTABLISHED, false );
			
		this.connectionManager.getXmppSettings().putBooleanData( XmppSettings.BOOLEAN_VALUE_AUTHENTICATED, false );
		
		for( int i = 0; i < this.getProtocolListeners().size(); i++ )
			this.getProtocolListeners().get( i ).disconnected( this.getAccountID() );
	}
	
	/* (non-Javadoc)
	 * @see de.karnik.libkara.pluginspecs.BaseProtocol#proto_disconnect()
	 */
	public void proto_disconnect() throws ProtocolException {
		
		
		if( this.connectionManager != null && this.connectionManager.isConnected() )
			try {
				this.connectionManager.disconnect();
			} catch (IOException e) {
				throw new ProtocolException( e );
			}
	}

	/* (non-Javadoc)
	 * @see de.karnik.libkara.pluginspecs.BaseProtocol#proto_getCompatibleMajorVersion()
	 */
	public int proto_getCompatibleMajorVersion() {
		return XmppProtocol.MAJOR_VERSION;
	}

	/* (non-Javadoc)
	 * @see de.karnik.libkara.pluginspecs.BaseProtocol#proto_getCompatibleMinorVersion()
	 */
	public int proto_getCompatibleMinorVersion() {
		return XmppProtocol.MINOR_VERSION;
	}
	
	/* (non-Javadoc)
	 * @see de.karnik.libkara.pluginspecs.BaseProtocol#proto_reconnect()
	 */
	public void proto_reconnect() throws ProtocolException {
		// TODO Auto-generated method stub
	}
	
	/* (non-Javadoc)
	 * @see de.karnik.libkara.proto.xmpp.XmppParserListener#serverAcceptsTls()
	 */
	public void tlsNegotationConfirmed() {
		try {
			if( this.connectionManager != null )
				this.connectionManager.startTLSNegotation();
			
			this.xmppReader = this.connectionManager.getXmppReader();
			this.xmppWriter = this.connectionManager.getXmppWriter();
			
			// reset Streams
			this.xmppParser.resetParser( this.xmppReader );
			this.writerThread.refreshXmppWriter( this.xmppWriter );
			
			this.xmppParser.sendStandardHeader();
		
		} catch ( Exception e ) {
			LogFunctions.log( "XmppProtocol.tlsNegotationConfirmed", "Exception", e.getMessage() );
			LogFunctions.log( "XmppProtocol.tlsNegotationConfirmed", "Exception", e.getStackTrace() );
		}
		
	}

	/* (non-Javadoc)
	 * @see de.karnik.libkara.proto.xmpp.XmppThreadListener#threadInterrupted(java.lang.String)
	 */
	public void threadInterrupted( String id ) {
		LogFunctions.log( "XmppProtocol.threadInterrupted", "Thread was interrupted. ID:", id );
	}

	/* (non-Javadoc)
	 * @see de.karnik.libkara.proto.xmpp.XmppThreadListener#threadStarted(java.lang.String)
	 */
	public void threadStarted( String id ) {
		LogFunctions.log( "XmppProtocol.threadInterrupted", "Thread was started. ID:", id );	
	}

	/* (non-Javadoc)
	 * @see de.karnik.libkara.proto.xmpp.XmppThreadListener#threadStopped(java.lang.String)
	 */
	public void threadStopped( String id ) {
		LogFunctions.log( "XmppProtocol.threadInterrupted", "Thread was stopped. ID:", id );
	}
	
	@Override
	public void proto_changeStatus( int status ) throws ProtocolException {
		this.connectionManager.getXmppSettings().putIntegerData( XmppSettings.INTEGER_VALUE_STATUS, status );
		
		XmppPresenceStanza presence = new XmppPresenceStanza();
		presence.setFrom( this.connectionManager.getXmppSettings().getCompleteUserID() );
		presence.setChildShow( 
				getXmppStatus( this.connectionManager.getXmppSettings().getIntegerData( XmppSettings.INTEGER_VALUE_STATUS ) ) );
		
		if( this.storage != null )
			this.storage.addOutputPackage( presence );
	
	}

	@Override
	public void proto_setStatus( int status ) throws ProtocolException {
		this.connectionManager.getXmppSettings().putIntegerData( XmppSettings.INTEGER_VALUE_STATUS, status );
	}

	@Override
	public void proto_refreshContactStatus( ImContact contact ) throws ProtocolException {
		XmppPresenceStanza presence = new XmppPresenceStanza();
		presence.setFrom( this.connectionManager.getXmppSettings().getCompleteUserID() );
		presence.setTo( contact.getUserID() );
		presence.setType( XmppPresenceStanza.TYPE_PROBE );
		this.storage.addOutputPackage( presence );
	}

	/* (non-Javadoc)
	 * @see de.karnik.libkara.proto.xmpp.packages.XmppStanzaReaderListener#contactStatusChanged(java.lang.String, int)
	 */
	public void contactStatusChanged( String userID, int status, String statusMessage ) {
		for( int i = 0; i < this.getProtocolListeners().size(); i++ )
			this.getProtocolListeners().get( i ).contactStatusChanged( userID, status, statusMessage );
		
	}
	
	/* (non-Javadoc)
	 * @see de.karnik.libkara.proto.xmpp.packages.XmppStanzaListener#connectionBound()
	 */
	public void connectionBound() {
		XmppIQStanza tempStanza = new XmppIQStanza();
		
		// if session is required start session negotation
		if( this.connectionManager.getXmppSettings().getBooleanData( XmppSettings.BOOLEAN_VALUE_SESSION_REQUIRED ) 
				&& this.connectionManager.getXmppSettings().getBooleanData( XmppSettings.BOOLEAN_VALUE_BOUND ) ) {
			
			String unique = StringFunctions.getUniqueString();
			
			this.connectionManager.getXmppSettings().putStringData( XmppSettings.STRING_VALUE_SESSIONID, unique );
			
			tempStanza.setType( XmppIQStanza.TYPE_SET );
			tempStanza.setFrom( this.connectionManager.getXmppSettings().getCompleteUserID() );
			tempStanza.setTo( this.connectionManager.getXmppSettings().getStringData( XmppSettings.STRING_VALUE_SERVICEID ) );
			tempStanza.setId( unique );
			tempStanza.setChild( new XmppIQChildSession() );
			
		// create stanza to retrieve roster
		} else {
			
			this.connectionManager.getXmppSettings().putBooleanData( XmppSettings.BOOLEAN_VALUE_LOGGED_IN, true );
			
			this.connectionManager.getXmppSettings()
				.putBooleanData( XmppSettings.BOOLEAN_VALUE_LOGGED_IN, true );
			tempStanza.setType( XmppIQStanza.TYPE_GET );
			tempStanza.setFrom( this.connectionManager.getXmppSettings().getCompleteUserID() );
			tempStanza.setId( XmppIQStanza.ID_GET_ROSTER );
			tempStanza.setChild( new XmppIQChildRoster() );	
			
			XmppPresenceStanza presence = new XmppPresenceStanza();
			presence.setFrom( this.connectionManager.getXmppSettings().getCompleteUserID() );
			presence.setChildShow( 
					getXmppStatus( this.connectionManager.getXmppSettings().getIntegerData( XmppSettings.INTEGER_VALUE_STATUS ) ) );
			this.storage.addOutputPackage( presence );
			
			for( int i = 0; i < this.getProtocolListeners().size(); i++ )
				this.getProtocolListeners().get( i ).connected( this.getAccountID() );

		}

		this.storage.addOutputPackage( tempStanza );
	}

	/* (non-Javadoc)
	 * @see de.karnik.libkara.proto.xmpp.packages.XmppStanzaListener#sessionEstablished()
	 */
	public void sessionEstablished() {

		this.connectionManager.getXmppSettings().putBooleanData( XmppSettings.BOOLEAN_VALUE_LOGGED_IN, true );
		
		// send stanza to retrieve roster
		XmppIQStanza tempStanza = new XmppIQStanza();
		
		tempStanza.setType( XmppIQStanza.TYPE_GET );
		tempStanza.setFrom( this.connectionManager.getXmppSettings().getCompleteUserID() );
		tempStanza.setId( XmppIQStanza.ID_GET_ROSTER );
		tempStanza.setChild( new XmppIQChildRoster() );
		
		this.storage.addOutputPackage( tempStanza );
		
		XmppPresenceStanza presence = new XmppPresenceStanza();
		presence.setFrom( this.connectionManager.getXmppSettings().getCompleteUserID() );
		presence.setChildShow( 
				getXmppStatus( this.connectionManager.getXmppSettings().getIntegerData( XmppSettings.INTEGER_VALUE_STATUS ) ) );
		this.storage.addOutputPackage( presence );
		
		for( int i = 0; i < this.getProtocolListeners().size(); i++ )
			this.getProtocolListeners().get( i ).connected( this.getAccountID() );
		
	}

	/* (non-Javadoc)
	 * @see de.karnik.libkara.proto.xmpp.packages.XmppStanzaReaderListener#rosterCompleted()
	 */
	public void rosterCompleted() {
		for( int i = 0; i < this.getProtocolListeners().size(); i++ )
			this.getProtocolListeners().get( i ).rosterReceived();
		
		XmlPackageStorage.enableHeartbeat = true;
	}

	/* (non-Javadoc)
	 * @see de.karnik.libkara.proto.xmpp.packages.XmppStanzaReaderListener#contactReceived(java.lang.String, java.lang.String, java.lang.String, java.lang.String[])
	 */
	public void contactReceived( String userID, String userName, String subscription, String[] groups ) {
		if( userName == null )
			userName = userID;
		
		// create contact item
		ImContact contact = new ImContact( userID, userName, groups );
		contact.setSubscription( subscription );
		
		for( int i = 0; i < this.getProtocolListeners().size(); i++ )
			this.getProtocolListeners().get( i ).contactReceived( contact );

	}

	/* (non-Javadoc)
	 * @see de.karnik.libkara.proto.xmpp.packages.XmppStanzaReaderListener#messageArrived(java.lang.String, de.karnik.libkara.roster.ImMessage)
	 */
	public void messageArrived(String userID, ImMessage message) {
		for( int i = 0; i < this.getProtocolListeners().size(); i++ )
			this.getProtocolListeners().get( i ).messageArrived( userID, message );
	}

	/* (non-Javadoc)
	 * @see de.karnik.libkara.pluginspecs.BaseProtocol#proto_sendMessage(de.karnik.libkara.roster.ImContact, de.karnik.libkara.roster.ImMessage)
	 */
	@Override
	public void proto_sendMessage( ImContact contact, ImMessage message ) throws ProtocolException {
		XmppMessageStanza xmppMessage = new XmppMessageStanza();
		xmppMessage.setType( XmppMessageStanza.TYPE_CHAT );
		xmppMessage.setTo( contact.getUserID() );
		xmppMessage.setFrom( this.connectionManager.getXmppSettings().getCompleteUserID() );
		xmppMessage.setDefaultChildBody( message.getMessage() );
		this.storage.addOutputPackage( xmppMessage );
	}
		
}
